// This file is part of OpenTSDB.
// Copyright (C) 2010-2012  The OpenTSDB Authors.
//
// This program is free software: you can redistribute it and/or modify it
// under the terms of the GNU Lesser General Public License as published by
// the Free Software Foundation, either version 2.1 of the License, or (at your
// option) any later version.  This program is distributed in the hope that it
// will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty
// of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Lesser
// General Public License for more details.  You should have received a copy
// of the GNU Lesser General Public License along with this program.  If not,
// see <http://www.gnu.org/licenses/>.
package net.opentsdb.tsd.client;

import com.google.gwt.event.dom.client.*;
import com.google.gwt.event.shared.EventHandler;
import com.google.gwt.user.client.ui.*;

import java.util.ArrayList;

final class MetricForm extends HorizontalPanel implements Focusable {

    public static interface MetricChangeHandler extends EventHandler {
        void onMetricChange(MetricForm widget);
    }

    private static final String TSDB_ID_CLASS = "[-_./a-zA-Z0-9]";
    private static final String TSDB_ID_RE = "^" + TSDB_ID_CLASS + "*$";
    private static final String TSDB_TAGVALUE_RE =
            "^(\\*?"                                       // a `*' wildcard or nothing
                    + "|" + TSDB_ID_CLASS + "+(\\|" + TSDB_ID_CLASS + "+)*)$"; // `foo|bar|...'

    private final EventsHandler events_handler;
    private MetricChangeHandler metric_change_handler;

    private final CheckBox downsample = new CheckBox("Downsample");
    private final ListBox downsampler = new ListBox();
    private final ValidatedTextBox interval = new ValidatedTextBox();
    private final CheckBox rate = new CheckBox("Rate");
    private final CheckBox x1y2 = new CheckBox("Right Axis");
    private final ListBox aggregators = new ListBox();
    private final ValidatedTextBox metric = new ValidatedTextBox();
    private final FlexTable tagtable = new FlexTable();

    public MetricForm(final EventsHandler handler) {
        events_handler = handler;
        setupDownsampleWidgets();
        downsample.addClickHandler(handler);
        downsampler.addChangeHandler(handler);
        interval.addBlurHandler(handler);
        interval.addKeyPressHandler(handler);
        rate.addClickHandler(handler);
        x1y2.addClickHandler(handler);
        aggregators.addChangeHandler(handler);
        metric.addBlurHandler(handler);
        metric.addKeyPressHandler(handler);
        {
            final EventsHandler metric_handler = new EventsHandler() {
                protected <H extends EventHandler> void onEvent(final DomEvent<H> event) {
                    if (metric_change_handler != null) {
                        metric_change_handler.onMetricChange(MetricForm.this);
                    }
                }
            };
            metric.addBlurHandler(metric_handler);
            metric.addKeyPressHandler(metric_handler);
        }

        metric.setValidationRegexp(TSDB_ID_RE);
        assembleUi();
    }

    public String getMetric() {
        return metric.getText();
    }

    /**
     * Parses the metric and tags out of the given string.
     *
     * @param metric A string of the form "metric" or "metric{tag=value,...}".
     * @return The name of the metric.
     */
    private String parseWithMetric(final String metric) {
        // TODO: Try to reduce code duplication with Tags.parseWithMetric().
        final int curly = metric.indexOf('{');
        if (curly < 0) {
            clearTags();
            return metric;
        }
        final int len = metric.length();
        if (metric.charAt(len - 1) != '}') {  // "foo{"
            clearTags();
            return null;  // Missing '}' at the end.
        } else if (curly == len - 2) {  // "foo{}"
            clearTags();
            return metric.substring(0, len - 2);
        }
        // substring the tags out of "foo{a=b,...,x=y}" and parse them.
        int i = 0;  // Tag index.
        final int num_tags_before = getNumTags();
        for (final String tag : metric.substring(curly + 1, len - 1).split(",")) {
            final String[] kv = tag.split("=");
            if (kv.length != 2 || kv[0].isEmpty() || kv[1].isEmpty()) {
                setTag(i, "", "");
                continue;  // Invalid tag.
            }
            if (i < num_tags_before) {
                setTag(i, kv[0], kv[1]);
            } else {
                addTag(kv[0], kv[1]);
            }
            i++;
        }
        // Leave an empty line at the end.
        if (i < num_tags_before) {
            setTag(i, "", "");
        } else {
            addTag();
        }
        // Remove extra tags.
        for (i++; i < num_tags_before; i++) {
            tagtable.removeRow(i + 1);
        }
        // Return the "foo" part of "foo{a=b,...,x=y}"
        return metric.substring(0, curly);
    }

    public void updateFromQueryString(final String m, final String o) {
        // TODO: Try to reduce code duplication with GraphHandler.parseQuery().
        // m is of the following forms:
        //   agg:[interval-agg:][rate:]metric[{tag=value,...}]
        // Where the parts in square brackets `[' .. `]' are optional.
        final String[] parts = m.split(":");
        final int nparts = parts.length;
        int i = parts.length;
        if (i < 2 || i > 4) {
            return;  // Malformed.
        }

        setSelectedItem(aggregators, parts[0]);

        i--;  // Move to the last part (the metric name).
        metric.setText(parseWithMetric(parts[i]));
        metric_change_handler.onMetricChange(this);

        final boolean rate = "rate".equals(parts[--i]);
        this.rate.setValue(rate, false);
        if (rate) {
            i--;
        }

        // downsampling function & interval.
        if (i > 0) {
            final int dash = parts[1].indexOf('-', 1);  // 1st char can't be `-'.
            if (dash < 0) {
                disableDownsample();
                return;  // Invalid downsampling specifier.
            }
            downsample.setValue(true, false);

            downsampler.setEnabled(true);
            setSelectedItem(downsampler, parts[1].substring(dash + 1));

            interval.setEnabled(true);
            interval.setText(parts[1].substring(0, dash));
        } else {
            disableDownsample();
        }

        x1y2.setValue(o.contains("axis x1y2"), false);
    }

    private void disableDownsample() {
        downsample.setValue(false, false);
        interval.setEnabled(false);
        downsampler.setEnabled(false);
    }

    public CheckBox x1y2() {
        return x1y2;
    }

    private void assembleUi() {
        setWidth("100%");
        {  // Left hand-side panel.
            final HorizontalPanel hbox = new HorizontalPanel();
            final InlineLabel l = new InlineLabel();
            l.setText("Metric:");
            hbox.add(l);
            final SuggestBox suggest = RemoteOracle.newSuggestBox("metrics",
                    metric);
            suggest.setLimit(40);
            hbox.add(suggest);
            hbox.setWidth("100%");
            metric.setWidth("100%");

            tagtable.setWidget(0, 0, hbox);
            tagtable.getFlexCellFormatter().setColSpan(0, 0, 3);
            addTag();
            tagtable.setText(1, 0, "Tags");
            add(tagtable);
        }
        {  // Right hand-side panel.
            final VerticalPanel vbox = new VerticalPanel();
            {
                final HorizontalPanel hbox = new HorizontalPanel();
                hbox.add(rate);
                hbox.add(x1y2);
                vbox.add(hbox);
            }
            {
                final HorizontalPanel hbox = new HorizontalPanel();
                final InlineLabel l = new InlineLabel();
                l.setText("Aggregator:");
                hbox.add(l);
                hbox.add(aggregators);
                vbox.add(hbox);
            }
            vbox.add(downsample);
            {
                final HorizontalPanel hbox = new HorizontalPanel();
                hbox.add(downsampler);
                hbox.add(interval);
                vbox.add(hbox);
            }
            add(vbox);
        }
    }

    public void setMetricChangeHandler(final MetricChangeHandler handler) {
        metric_change_handler = handler;
    }

    public void setAggregators(final ArrayList<String> aggs) {
        for (final String agg : aggs) {
            aggregators.addItem(agg);
            downsampler.addItem(agg);
        }
        setSelectedItem(aggregators, "sum");
        setSelectedItem(downsampler, "avg");
    }

    public boolean buildQueryString(final StringBuilder url) {
        final String metric = getMetric();
        if (metric.isEmpty()) {
            return false;
        }
        url.append("&m=");
        url.append(selectedValue(aggregators));
        if (downsample.getValue()) {
            url.append(':').append(interval.getValue())
                    .append('-').append(selectedValue(downsampler));
        }
        if (rate.getValue()) {
            url.append(":rate");
        }
        url.append(':').append(metric);
        {
            final int ntags = getNumTags();
            url.append('{');
            for (int tag = 0; tag < ntags; tag++) {
                final String tagname = getTagName(tag);
                final String tagvalue = getTagValue(tag);
                if (tagname.isEmpty() || tagvalue.isEmpty()) {
                    continue;
                }
                url.append(tagname).append('=').append(tagvalue)
                        .append(',');
            }
            final int last = url.length() - 1;
            if (url.charAt(last) == '{') {  // There was no tag.
                url.setLength(last);          // So remove the `{'.
            } else {  // Need to replace the last `,' with a `}'.
                url.setCharAt(url.length() - 1, '}');
            }
        }
        url.append("&o=");
        if (x1y2.getValue()) {
            url.append("axis x1y2");
        }
        return true;
    }

    private int getNumTags() {
        return tagtable.getRowCount() - 1;
    }

    private String getTagName(final int i) {
        return ((SuggestBox) tagtable.getWidget(i + 1, 1)).getValue();
    }

    private String getTagValue(final int i) {
        return ((SuggestBox) tagtable.getWidget(i + 1, 2)).getValue();
    }

    private void setTagName(final int i, final String value) {
        ((SuggestBox) tagtable.getWidget(i + 1, 1)).setValue(value);
    }

    private void setTagValue(final int i, final String value) {
        ((SuggestBox) tagtable.getWidget(i + 1, 2)).setValue(value);
    }

    /**
     * Changes the name/value of an existing tag.
     *
     * @param i     The index of the tag to change.
     * @param name  The new name of the tag.
     * @param value The new value of the tag.
     *              Requires: {@code i < getNumTags()}.
     */
    private void setTag(final int i, final String name, final String value) {
        setTagName(i, name);
        setTagValue(i, value);
    }

    private void addTag() {
        addTag(null, null);
    }

    private void addTag(final String default_tagname) {
        addTag(default_tagname, null);
    }

    private void addTag(final String default_tagname,
                        final String default_value) {
        final int row = tagtable.getRowCount();

        final ValidatedTextBox tagname = new ValidatedTextBox();
        final SuggestBox suggesttagk = RemoteOracle.newSuggestBox("tagk", tagname);
        final ValidatedTextBox tagvalue = new ValidatedTextBox();
        final SuggestBox suggesttagv = RemoteOracle.newSuggestBox("tagv", tagvalue);
        tagname.setValidationRegexp(TSDB_ID_RE);
        tagvalue.setValidationRegexp(TSDB_TAGVALUE_RE);
        tagname.setWidth("100%");
        tagvalue.setWidth("100%");
        tagname.addBlurHandler(recompact_tagtable);
        tagname.addBlurHandler(events_handler);
        tagname.addKeyPressHandler(events_handler);
        tagvalue.addBlurHandler(recompact_tagtable);
        tagvalue.addBlurHandler(events_handler);
        tagvalue.addKeyPressHandler(events_handler);

        tagtable.setWidget(row, 1, suggesttagk);
        tagtable.setWidget(row, 2, suggesttagv);
        if (row > 2) {
            final Button remove = new Button("x");
            remove.addClickHandler(removetag);
            tagtable.setWidget(row - 1, 0, remove);
        }
        if (default_tagname != null) {
            tagname.setText(default_tagname);
            if (default_value == null) {
                tagvalue.setFocus(true);
            }
        }
        if (default_value != null) {
            tagvalue.setText(default_value);
        }
    }

    private void clearTags() {
        setTag(0, "", "");
        for (int i = getNumTags() - 1; i > 1; i++) {
            tagtable.removeRow(i + 1);
        }
    }

    public void autoSuggestTag(final String tag) {
        // First try to see if the tag is already in the table.
        final int nrows = tagtable.getRowCount();
        int unused_row = -1;
        for (int row = 1; row < nrows; row++) {
            final SuggestBox tagname = ((SuggestBox) tagtable.getWidget(row, 1));
            final SuggestBox tagvalue = ((SuggestBox) tagtable.getWidget(row, 2));
            final String thistag = tagname.getValue();
            if (thistag.equals(tag)) {
                return;  // This tag is already in the table.
            }
            if (thistag.isEmpty() && tagvalue.getValue().isEmpty()) {
                unused_row = row;
                break;
            }
        }
        if (unused_row >= 0) {
            ((SuggestBox) tagtable.getWidget(unused_row, 1)).setValue(tag);
        } else {
            addTag(tag);
        }
    }

    private final BlurHandler recompact_tagtable = new BlurHandler() {
        public void onBlur(final BlurEvent event) {
            int ntags = getNumTags();
            // Is the first line empty?  If yes, move everything up by 1 line.
            if (getTagName(0).isEmpty() && getTagValue(0).isEmpty()) {
                for (int tag = 1; tag < ntags; tag++) {
                    final String tagname = getTagName(tag);
                    final String tagvalue = getTagValue(tag);
                    setTag(tag - 1, tagname, tagvalue);
                }
                setTag(ntags - 1, "", "");
            }
            // Try to remove empty lines from the tag table (but never remove the
            // first line or last line, even if they're empty).  Walk the table
            // from the end to make it easier to delete rows as we iterate.
            for (int tag = ntags - 1; tag >= 1; tag--) {
                final String tagname = getTagName(tag);
                final String tagvalue = getTagValue(tag);
                if (tagname.isEmpty() && tagvalue.isEmpty()) {
                    tagtable.removeRow(tag + 1);
                }
            }
            ntags = getNumTags();  // How many lines are left?
            // If the last line isn't empty, save another one.
            final String tagname = getTagName(ntags - 1);
            final String tagvalue = getTagValue(ntags - 1);
            if (!tagname.isEmpty() && !tagvalue.isEmpty()) {
                addTag();
            }
        }
    };

    private final ClickHandler removetag = new ClickHandler() {
        public void onClick(final ClickEvent event) {
            if (!(event.getSource() instanceof Button)) {
                return;
            }
            final Widget source = (Widget) event.getSource();
            final int ntags = getNumTags();
            for (int tag = 1; tag < ntags; tag++) {
                if (source == tagtable.getWidget(tag + 1, 0)) {
                    tagtable.removeRow(tag + 1);
                    events_handler.onClick(event);
                    break;
                }
            }
        }
    };

    private void setupDownsampleWidgets() {
        downsampler.setEnabled(false);
        interval.setEnabled(false);
        interval.setMaxLength(5);
        interval.setVisibleLength(5);
        interval.setValue("10m");
        interval.setValidationRegexp("^[1-9][0-9]*[smhdwy]$");
        downsample.addClickHandler(new ClickHandler() {
            public void onClick(final ClickEvent event) {
                final boolean checked = ((CheckBox) event.getSource()).getValue();
                downsampler.setEnabled(checked);
                interval.setEnabled(checked);
                if (checked) {
                    downsampler.setFocus(true);
                }
            }
        });
    }

    private static String selectedValue(final ListBox list) {  // They should save
        return list.getValue(list.getSelectedIndex());           // this to GWT...
    }

    /**
     * If the given item is in the list, mark it as selected.
     *
     * @param list The list to manipulate.
     * @param item The item to select if present.
     */
    private void setSelectedItem(final ListBox list, final String item) {
        final int nitems = list.getItemCount();
        for (int i = 0; i < nitems; i++) {
            if (item.equals(list.getValue(i))) {
                list.setSelectedIndex(i);
                return;
            }
        }
    }

    // ------------------- //
    // Focusable interface //
    // ------------------- //

    public int getTabIndex() {
        return metric.getTabIndex();
    }

    public void setTabIndex(final int index) {
        metric.setTabIndex(index);
    }

    public void setAccessKey(final char key) {
        metric.setAccessKey(key);
    }

    public void setFocus(final boolean focused) {
        metric.setFocus(focused);
    }

}
